package hn.sigit.logic.geometry;

import hn.sigit.model.commons.IParcel;
import hn.sigit.model.commons.IProperty;
import hn.sigit.model.hnd.ladmshadow.Parcel;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;

import org.hibernatespatial.mgeom.MCoordinate;

import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.LinearRing;
import com.vividsolutions.jts.geom.MultiLineString;
import com.vividsolutions.jts.geom.MultiPolygon;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.Polygon;
import com.vividsolutions.jts.geom.PrecisionModel;

public class GeometryOperations {
	public static final double EPSILON = 0.001;

	//TODO: Parametrizar el SRID del geometry factory!
	public static final int CURRENT_SRID = 32616;
	public static final GeometryFactory geomFactory = new GeometryFactory(new PrecisionModel(), CURRENT_SRID);
	
	public static Polygon mergePolygons(Polygon poly1, Polygon poly2) throws MergeException {
		Polygon newPoly = null;
		
		if ( poly1.touches(poly2) && poly1.intersection(poly2).getLength() > 0.001) {
			newPoly = (Polygon) poly1.union(poly2);
			
			//correct m and z coordinate member to avoid dimensionality problems
			LineString lsExterior = newPoly.getExteriorRing();
			Coordinate[] exteriorCoords = lsExterior.getCoordinates();
			for (int i = 0; i < exteriorCoords.length; i++) {
				MCoordinate sampleCoord = (MCoordinate) poly1.getCoordinate();
				if ( !(exteriorCoords[i] instanceof MCoordinate) )
					exteriorCoords[i] = new MCoordinate(exteriorCoords[i]);
				
				((MCoordinate) exteriorCoords[i]).m = sampleCoord.m;
				((MCoordinate) exteriorCoords[i]).z = sampleCoord.z;
			}
			
			//TODO: agregar soporte para holes. DAMN union operation
			//does not retain the MCoordinate type!
			lsExterior = GeometryOperations.geomFactory.createLinearRing(exteriorCoords);
			newPoly = GeometryOperations.geomFactory.createPolygon((LinearRing) lsExterior, null);
		}
		else {
			throw new MergeException("dataentry.merge.error_nonadjacent_parcels");
		}
		
		return newPoly;
	}
	
	//used to avoid undershoot border problems when splitting
	//border is added 1 meter at the beginning/ending sides
	public static void adjustSplitterCoordinates(Coordinate[] splitterCoordinates) {
		if (splitterCoordinates.length < 2) return;
		
		int firstIdx = 0;
		int lastIdx = splitterCoordinates.length - 1;
		
		Coordinate adjustedFirstCoord = addPointDistance(
				splitterCoordinates[firstIdx + 1],
				splitterCoordinates[firstIdx],
				1.0);
		Coordinate adjustedLastCoord = addPointDistance(
				splitterCoordinates[lastIdx - 1],
				splitterCoordinates[lastIdx],
				1.0);
		
		splitterCoordinates[firstIdx] = adjustedFirstCoord;
		splitterCoordinates[lastIdx] = adjustedLastCoord;
	}
	
	public static SplitPolygonResult splitPolygon(Polygon poly, Coordinate[] splitterCoordinates) throws NoSplitException, InvalidSplitException {
		Polygon[] result = null;
		
		//TODO: ver si se conserva la asumcion del MCoordinate
		Coordinate firstCoord = poly.getCoordinate();
		if (firstCoord instanceof MCoordinate) {
			
			for (Coordinate coord : splitterCoordinates) {
				((MCoordinate) coord).m = ((MCoordinate) firstCoord).m;
				coord.z = firstCoord.z;
			}
		}
		else {
			for (Coordinate coord : splitterCoordinates)
				coord.z = firstCoord.z;
		}
		
		//do splitter coordinates adjustment to avoid undershoots
		adjustSplitterCoordinates(splitterCoordinates);
		LineString splitter = GeometryOperations.geomFactory.createLineString(splitterCoordinates);
		
		if (!poly.intersects(splitter)) {
			//The splitter does not intecept the poly, so no split occurs
			throw new NoSplitException();
		}
		
		Polygon diffPoly = (Polygon) poly.difference(splitter);
		LinearRing[] holes = null;
		
		//Calculate the resulting boundary between the 2 new polygons
		//and check that the result geometry is a LineString considering that
		//if the polygon is not split into 2, then the result is a MultiLineString
		Object objBoundary = splitter.intersection(poly);
		if (!(objBoundary instanceof LineString))
			throw new InvalidSplitException();

		LineString boundary = (LineString) objBoundary;
		
		int numBoundaryPoints = boundary.getNumPoints();
		Coordinate firstBoundaryCoord = boundary.getCoordinateN(0);
		Coordinate lastBoundaryCoord = boundary.getCoordinateN(numBoundaryPoints - 1);

		Coordinate[] boundaryCoords = boundary.getCoordinates();
		Coordinate[] diffPolyCoords = diffPoly.getCoordinates();
		
		
		//Test to see if at most 2 the first and last boundary coordinates appear
		//in the polygon-linestring difference polygon
		int countSharedCoords = 0;
		Set<Coordinate> s = new HashSet<Coordinate>(Arrays.asList(diffPolyCoords));
		for (Coordinate c : boundaryCoords) {
			if (s.contains(c))
				countSharedCoords++;
		}
		if (countSharedCoords < 2)
			throw new NoSplitException();
		else if (countSharedCoords > 2)
			throw new InvalidSplitException();
			
		
		ArrayList<Coordinate> vertList1 = new ArrayList<Coordinate>();
		ArrayList<Coordinate> vertList2 = new ArrayList<Coordinate>();
		
		Stack<Coordinate> p = new Stack<Coordinate>();

		int addingTo = 0;
		for (Coordinate coord : diffPolyCoords) {
			if (coord.equals(firstBoundaryCoord)) {
				vertList1.add(coord);
				vertList2.add(coord);
				
				if (numBoundaryPoints > 2) {
					if (0 == addingTo) {
						for (int i = 1; i < numBoundaryPoints - 1; i++)
							vertList1.add(p.push(boundary.getCoordinateN(i)));
					}
					else {
						while (!p.isEmpty())
							vertList2.add(p.pop());
					}
				}
				
				addingTo = addingTo * -1 + 1;
			}
			else if (coord.equals(lastBoundaryCoord)) {
				vertList1.add(coord);
				vertList2.add(coord);
				
				if (numBoundaryPoints > 2) {
					if (0 == addingTo) {
						for (int i = numBoundaryPoints - 2; i > 0; i--)
							vertList1.add(p.push(boundary.getCoordinateN(i)));
					}
					else {
						while (!p.isEmpty())
							vertList2.add(p.pop());
					}
				}

				addingTo = addingTo * -1 + 1;
			}
			else {
				if (0 == addingTo)
					vertList1.add(coord);
				else
					vertList2.add(coord);
			}
		}
		
		if ( !vertList2.get(0).equals( vertList2.get( vertList2.size()-1 ) ) )
			vertList2.add(vertList2.get(0));
		
		//TODO: ver lo del MCoordinate...
		MCoordinate[] poly1Coords = new MCoordinate[vertList1.size()];
		for (int i = 0; i < poly1Coords.length; i++) {
			Coordinate c = vertList1.get(i);
			poly1Coords[i] = new MCoordinate(c.x, c.y, firstCoord.z, ((MCoordinate) firstCoord).m);
		}
		
		MCoordinate[] poly2Coords = new MCoordinate[vertList2.size()]; 
		for (int i = 0; i < poly2Coords.length; i++) {
			Coordinate c = vertList2.get(i);
			poly2Coords[i] = new MCoordinate(c.x, c.y, firstCoord.z, ((MCoordinate) firstCoord).m);
		}
		
		result = new Polygon[2];
		result[0] = GeometryOperations.geomFactory.createPolygon(GeometryOperations.geomFactory.createLinearRing(poly1Coords), holes);
		result[1] = GeometryOperations.geomFactory.createPolygon(GeometryOperations.geomFactory.createLinearRing(poly2Coords), holes);
		
		result[0].setSRID(poly.getSRID());
		result[1].setUserData(poly.getUserData());
		
		return new SplitPolygonResult(poly, diffPoly, result[0], result[1], firstBoundaryCoord, lastBoundaryCoord);
	}
	
	public static Polygon addCoordinateToPolygon(Polygon thePolygon, MCoordinate theCoordinate) {
		Point thePoint = GeometryOperations.geomFactory.createPoint(theCoordinate);

		Polygon newPolygon = null;
		LineString lsExteriorRing = thePolygon.getExteriorRing();
		
		LineString testLine;
		MCoordinate[] testLineCoordinates = new MCoordinate[2];
		
		double minDistance = Double.MAX_VALUE;
		double distance;
		int minCoordinateIdx = 0;
		int numPoints = lsExteriorRing.getNumPoints();
		for (int i = 0; i < numPoints-1; i++) {
			testLineCoordinates[0] = (MCoordinate) lsExteriorRing.getCoordinateN(i);
			testLineCoordinates[1] = (MCoordinate) lsExteriorRing.getCoordinateN(i+1);
			
			testLine = GeometryOperations.geomFactory.createLineString(testLineCoordinates);
			
			distance = thePoint.distance(testLine);
			if (distance < minDistance) {
				minDistance = distance;
				minCoordinateIdx = i;
			}
		}
		
		//generate new corrected polygon
		MCoordinate[] newPolygonCoordinates = new MCoordinate[numPoints + 1];
		for (int i = 0, j = 0; i < numPoints; i++, j++) {
			newPolygonCoordinates[j] = (MCoordinate) lsExteriorRing.getCoordinateN(i);
			if (i == minCoordinateIdx)
				newPolygonCoordinates[++j] = theCoordinate;
		}
		
		LinearRing newPolygonLinearRing = GeometryOperations.geomFactory.createLinearRing(newPolygonCoordinates);
		
		//TODO: considerar el caso con anillos internos
		newPolygon = GeometryOperations.geomFactory.createPolygon(newPolygonLinearRing, null);
		
		return newPolygon;
	}
	
	public static Map<Long, Parcel> eliminateGaps(Parcel splitParcel, List<Parcel> pAdjacentParcels, SplitPolygonResult spr) {
		Map<Long,Parcel> correctedParcelMap = new HashMap<Long,Parcel>();
		List<Parcel> adjacentParcels = new ArrayList<Parcel>(pAdjacentParcels);
		List<Parcel> clonedAdjacentParcels = new ArrayList<Parcel>();
		
		double minDistance;
		double distance;
		Parcel closestParcel;

		Point[] boundaryPoints = new Point[2];
		boundaryPoints[0] = GeometryOperations.geomFactory.createPoint(spr.getFirstBoundaryCoord());
		boundaryPoints[1] = GeometryOperations.geomFactory.createPoint(spr.getLastBoundaryCoord());
		
		/*
		List<Parcel> adjacentParcels = new ArrayList<Parcel>(pAdjacentParcels);
		for (int i = 0; i < adjacentParcels.size(); i++)
			adjacentParcels.set(i, adjacentParcels.get(i).clone());
		*/
		
		//correcion con el primer punto
		for (Point p : boundaryPoints) {
			closestParcel = null;
			minDistance = Double.MAX_VALUE;
			for (Parcel parcel : adjacentParcels) {
				distance = p.distance(parcel.getShape());
				if (distance < minDistance) {
					minDistance = distance;
					closestParcel = parcel;
				}
			}
			if (closestParcel != null) {
				Polygon currentShape = closestParcel.getShape();
				MCoordinate newShapeCoord = new MCoordinate(p.getCoordinate());
				newShapeCoord.m = ((MCoordinate) currentShape.getCoordinate()).m;
				newShapeCoord.z = currentShape.getCoordinate().z;
				Polygon newShape = addCoordinateToPolygon(currentShape, newShapeCoord);
				
				Geometry testGeom = spr.getDifferencedPolygon().intersection(newShape);
				//TODO: Improve test condition considering all posibilities!
				if ( testGeom instanceof LineString || testGeom instanceof MultiLineString ) {
					Parcel clonedClosestParcel = closestParcel.clone();
					clonedClosestParcel.setSuID(closestParcel.getSuID());
					
					correctedParcelMap.put(closestParcel.getSuID(), clonedClosestParcel);
					
					clonedClosestParcel.setShape(newShape);
					clonedClosestParcel.setModified(true);
					
					adjacentParcels.remove(closestParcel);
					adjacentParcels.add(clonedClosestParcel);
					clonedAdjacentParcels.add(clonedClosestParcel);
				}
			}
		}
		
		for (Parcel p : clonedAdjacentParcels)
			p.setSuID(0);
		
		return correctedParcelMap;
	}
	
	public static MultiPolygon getParcelsFromPropertyAsMultiPolygon(IProperty property) {
		Set<IParcel> parcels = property.getParcels();
		
		if (parcels != null && parcels.size() > 0) {
			Polygon[] polygons = new Polygon[parcels.size()];
			int i = 0;
			for (IParcel hndP : parcels)
				polygons[i++] = hndP.getShape();
			
			return GeometryOperations.geomFactory.createMultiPolygon(polygons);
		}
		
		return null;
	}

	//returns the adjusted coordinate
	public static Coordinate setPointDistance(Coordinate p1, Coordinate p2, double distUnits) {
		double norm = distance(p1, p2);
		
		Coordinate newCoord = (Coordinate) p1.clone();
		
		double dx = (p2.x - p1.x) / norm * distUnits; 
		double dy = (p2.y - p1.y) / norm * distUnits;
		
		newCoord.x = p1.x + dx;
		newCoord.y = p1.y + dy;
		
		return newCoord;
	}
	
	public static Coordinate addPointDistance(Coordinate p1, Coordinate p2, double distUnits) {
		double norm = distance(p1, p2);
		
		Coordinate newCoord = (Coordinate) p1.clone();
		
		double dx = (p2.x - p1.x) / norm * (distUnits + norm); 
		double dy = (p2.y - p1.y) / norm * (distUnits + norm);
		
		newCoord.x = p1.x + dx;
		newCoord.y = p1.y + dy;
		
		return newCoord;
	}

	public static double distance(double x1, double y1, double x2, double y2) {
		return Math.sqrt((x2-x1)*(x2-x1) + (y2-y1)*(y2-y1));
	}
	public static double distance(Coordinate p1, Coordinate p2) {
		return distance(p1.x, p1.y, p2.x, p2.y);
	}
	
	public static double directionToRadAngle(String direction) {
		direction = direction.trim();
		double angle = 0;
		
		StringBuilder numberPart = new StringBuilder();
		StringBuilder orientationPart = new StringBuilder();
		
		char ch;
		for (int i = 0; i < direction.length(); i++) {
			ch = direction.charAt(i);
			
			if (Character.isDigit(ch) || ch == ',' || ch == '.')
				numberPart.append(ch);
			else if ("NSEWnsewOo".indexOf(ch) != -1)
				orientationPart.append(ch);
			else if ("° ".indexOf(ch) != -1)
				; //do nothing
			else
				throw new IllegalArgumentException();
		}
		
		try {
			double number = Double.parseDouble(numberPart.toString()) * Math.PI / 180;
			if (number < 0.0)
				throw new IllegalArgumentException();
			
			String orientation = orientationPart.toString().toUpperCase();
			
			if (orientation.equals("N") && angle < EPSILON)
				angle = Math.PI / 2;
			else if (orientation.equals("S") && angle < EPSILON)
				angle = 3 * Math.PI / 2;
			else if (orientation.equals("NE"))
				angle = Math.PI / 2 - number;
			else if (orientation.equals("NW") || orientation.equals("NO"))
				angle = Math.PI / 2 + number;
			else if (orientation.equals("SW") || orientation.equals("SO"))
				angle = 3 * Math.PI / 2 - number;
			else if (orientation.equals("SE"))
				angle = 3 * Math.PI / 2 + number;
			else
				throw new IllegalArgumentException();
		}
		catch (NumberFormatException e) {
			throw new IllegalArgumentException(e);
		}
		
		return angle;
	}


	public static double azimuth(double x1, double y1, double x2, double y2) {
		double a = 0; //90 deg since we measure theta from the Y-axis
		
		double delta_x = Math.abs(x2 - x1);
		if (delta_x > EPSILON) {
			double dist = distance(x1, y1, x2, y2);
			a = Math.asin(delta_x / dist);
		}

		return a;
	}
	public static double azimuth(Coordinate p1, Coordinate p2) {
		return azimuth(p1.x, p1.y, p2.x, p2.y);
	}
	
	public static double theta(double x1, double y1, double x2, double y2) {
		double dist = distance(x1, y1, x2, y2);
		double dx = (x2 - x1) / dist;
		double dy = (y2 - y1) / dist;
		double t = 0;
		if (Math.abs(dx) > EPSILON && Math.abs(dy) > EPSILON) {
			t = dy > 0 ? Math.acos(dx) : 2 * Math.PI - Math.acos(dx);  
		}
		else if (Math.abs(dx) > EPSILON) { //dy = 0. Either 0 or PI
			t += dx > 0 ? 0 : Math.PI;
		}
		else { //dx = 0. Either PI/2 or 3PI/2
			t += dy > 0 ? Math.PI : 3 * Math.PI / 2;
		}
		
		return t;
	}
	public static double theta(Coordinate p1, Coordinate p2) {
		return theta(p1.x, p1.y, p2.x, p2.y);
	}

	public static String bearingAndDistance(double radAngle, double diff_x, double diff_y) {
		double thetaDegs = radAngle * 180 / Math.PI;
		String result = "";
		if (Math.abs(thetaDegs - 90.0) > EPSILON) {
			result += "" + thetaDegs + "° ";
			if (diff_y > EPSILON)
				result += "N";
			else if (diff_y < -EPSILON)
				result += "S";

			if (diff_x > EPSILON)
				result += " E";
			else if (diff_x < -EPSILON)
				result += " W";
		}
		else if (Math.abs(thetaDegs - 0.0) < EPSILON) {
			result += "0.0° ";
			if (diff_y > 0)
				result += "N";
			else
				result += "S";
		}
		else {
			result += "90.0° ";
			if (diff_x > 0)
				result += "E";
			else
				result += "W";
		}
		
		return result;
	}
	public static String bearingAndDistance(double x1, double y1, double x2, double y2) {
		double diff_x = Math.abs(x2 - x1);
		double diff_y = Math.abs(y2 - y1);
		double radAngle = theta(x1, y1, x2, y2);
		
		return bearingAndDistance(radAngle, diff_x, diff_y);
	}
	public static String bearingAndDistance(Coordinate p1, Coordinate p2) {
		return bearingAndDistance(p1.x, p1.y, p2.x, p2.y);
	}
	
	public static double degToRad(double deg) {
		return deg * Math.PI / 180.0;
	}
	public static double radToDeg(double rad) {
		return rad * 180.0 / Math.PI;
	}
}
